/**
 * Copyright 2009-2018 the original author or authors.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ibatis.builder;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.util.*;

/**
 * @author Clinton Begin
 */
public class MapperBuilderAssistant extends BaseBuilder {

	private String currentNamespace;

	private final String resource;

	private Cache currentCache;

	private boolean unresolvedCacheRef; // issue #676

	public MapperBuilderAssistant(Configuration configuration, String resource) {
		super(configuration);
		ErrorContext.instance().resource(resource);
		this.resource = resource;
	}

	public String getCurrentNamespace() {
		return currentNamespace;
	}

	public void setCurrentNamespace(String currentNamespace) {
		if (currentNamespace == null) {
			throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
		}

		if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
			throw new BuilderException("Wrong namespace. Expected '"
					+ this.currentNamespace + "' but found '" + currentNamespace + "'.");
		}

		this.currentNamespace = currentNamespace;
	}

	public String applyCurrentNamespace(String base, boolean isReference) {
		if (base == null) {
			return null;
		}
		if (isReference) {
			// is it qualified with any namespace yet?
			if (base.contains(".")) {
				return base;
			}
		} else {
			// is it qualified with this namespace yet?
			if (base.startsWith(currentNamespace + ".")) {
				return base;
			}
			if (base.contains(".")) {
				throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
			}
		}
		return currentNamespace + "." + base;
	}

	public Cache useCacheRef(String namespace) {
		if (namespace == null) {
			throw new BuilderException("cache-ref element requires a namespace attribute.");
		}
		try {
			unresolvedCacheRef = true;
			Cache cache = configuration.getCache(namespace);
			if (cache == null) {
				throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
			}
			currentCache = cache;
			unresolvedCacheRef = false;
			return cache;
		} catch (IllegalArgumentException e) {
			throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
		}
	}

	public Cache useNewCache(Class<? extends Cache> typeClass,
							 Class<? extends Cache> evictionClass,
							 Long flushInterval,
							 Integer size,
							 boolean readWrite,
							 boolean blocking,
							 Properties props) {
		Cache cache = new CacheBuilder(currentNamespace)
				.implementation(valueOrDefault(typeClass, PerpetualCache.class))
				.addDecorator(valueOrDefault(evictionClass, LruCache.class))
				.clearInterval(flushInterval)
				.size(size)
				.readWrite(readWrite)
				.blocking(blocking)
				.properties(props)
				.build();
		configuration.addCache(cache);
		currentCache = cache;
		return cache;
	}

	public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
		id = applyCurrentNamespace(id, false);
		ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
		configuration.addParameterMap(parameterMap);
		return parameterMap;
	}

	public ParameterMapping buildParameterMapping(
			Class<?> parameterType,
			String property,
			Class<?> javaType,
			JdbcType jdbcType,
			String resultMap,
			ParameterMode parameterMode,
			Class<? extends TypeHandler<?>> typeHandler,
			Integer numericScale) {
		resultMap = applyCurrentNamespace(resultMap, true);

		// Class parameterType = parameterMapBuilder.type();
		Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType);
		TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);

		return new ParameterMapping.Builder(configuration, property, javaTypeClass)
				.jdbcType(jdbcType)
				.resultMapId(resultMap)
				.mode(parameterMode)
				.numericScale(numericScale)
				.typeHandler(typeHandlerInstance)
				.build();
	}

	public ResultMap addResultMap(
			String id,
			Class<?> type,
			String extend,
			Discriminator discriminator,
			List<ResultMapping> resultMappings,
			Boolean autoMapping) {
		id = applyCurrentNamespace(id, false);
		extend = applyCurrentNamespace(extend, true);

		if (extend != null) {
			// 如果继承了其他ResultMap
			if (!configuration.hasResultMap(extend)) {
				throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
			}
			// 获取继承的父ResultMap对象
			ResultMap resultMap = configuration.getResultMap(extend);
			List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());
			extendedResultMappings.removeAll(resultMappings);
			// 如果父ResultMap定义了构造器映射，则移除构造器映射.
			boolean declaresConstructor = false;
			for (ResultMapping resultMapping : resultMappings) {
				if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
					declaresConstructor = true;
					break;
				}
			}
			if (declaresConstructor) {
				Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();
				while (extendedResultMappingsIter.hasNext()) {
					if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
						extendedResultMappingsIter.remove();
					}
				}
			}
			// 將父ResultMap配置的映射信息添加到当前ResultMap
			resultMappings.addAll(extendedResultMappings);
		}
		// 通过建造者模式创建ResultMap对象
		ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
				.discriminator(discriminator)
				.build();
		configuration.addResultMap(resultMap);
		return resultMap;
	}

	public Discriminator buildDiscriminator(
			Class<?> resultType,
			String column,
			Class<?> javaType,
			JdbcType jdbcType,
			Class<? extends TypeHandler<?>> typeHandler,
			Map<String, String> discriminatorMap) {
		ResultMapping resultMapping = buildResultMapping(
				resultType,
				null,
				column,
				javaType,
				jdbcType,
				null,
				null,
				null,
				null,
				typeHandler,
				new ArrayList<ResultFlag>(),
				null,
				null,
				false);
		Map<String, String> namespaceDiscriminatorMap = new HashMap<String, String>();
		for (Map.Entry<String, String> e : discriminatorMap.entrySet()) {
			String resultMap = e.getValue();
			resultMap = applyCurrentNamespace(resultMap, true);
			namespaceDiscriminatorMap.put(e.getKey(), resultMap);
		}
		return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();
	}

	public MappedStatement addMappedStatement(
			String id,
			SqlSource sqlSource,
			StatementType statementType,
			SqlCommandType sqlCommandType,
			Integer fetchSize,
			Integer timeout,
			String parameterMap,
			Class<?> parameterType,
			String resultMap,
			Class<?> resultType,
			ResultSetType resultSetType,
			boolean flushCache,
			boolean useCache,
			boolean resultOrdered,
			KeyGenerator keyGenerator,
			String keyProperty,
			String keyColumn,
			String databaseId,
			LanguageDriver lang,
			String resultSets) {

		if (unresolvedCacheRef) {
			throw new IncompleteElementException("Cache-ref not yet resolved");
		}

		id = applyCurrentNamespace(id, false);
		boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

		MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
				.resource(resource)
				.fetchSize(fetchSize)
				.timeout(timeout)
				.statementType(statementType)
				.keyGenerator(keyGenerator)
				.keyProperty(keyProperty)
				.keyColumn(keyColumn)
				.databaseId(databaseId)
				.lang(lang)
				.resultOrdered(resultOrdered)
				.resultSets(resultSets)
				.resultMaps(getStatementResultMaps(resultMap, resultType, id))
				.resultSetType(resultSetType)
				.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
				.useCache(valueOrDefault(useCache, isSelect))
				.cache(currentCache);

		ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
		if (statementParameterMap != null) {
			statementBuilder.parameterMap(statementParameterMap);
		}

		MappedStatement statement = statementBuilder.build();
		configuration.addMappedStatement(statement);
		return statement;
	}

	private <T> T valueOrDefault(T value, T defaultValue) {
		return value == null ? defaultValue : value;
	}

	private ParameterMap getStatementParameterMap(
			String parameterMapName,
			Class<?> parameterTypeClass,
			String statementId) {
		parameterMapName = applyCurrentNamespace(parameterMapName, true);
		ParameterMap parameterMap = null;
		if (parameterMapName != null) {
			try {
				parameterMap = configuration.getParameterMap(parameterMapName);
			} catch (IllegalArgumentException e) {
				throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e);
			}
		} else if (parameterTypeClass != null) {
			List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
			parameterMap = new ParameterMap.Builder(
					configuration,
					statementId + "-Inline",
					parameterTypeClass,
					parameterMappings).build();
		}
		return parameterMap;
	}

	private List<ResultMap> getStatementResultMaps(
			String resultMap,
			Class<?> resultType,
			String statementId) {
		resultMap = applyCurrentNamespace(resultMap, true);

		List<ResultMap> resultMaps = new ArrayList<ResultMap>();
		if (resultMap != null) {
			String[] resultMapNames = resultMap.split(",");
			for (String resultMapName : resultMapNames) {
				try {
					resultMaps.add(configuration.getResultMap(resultMapName.trim()));
				} catch (IllegalArgumentException e) {
					throw new IncompleteElementException("Could not find result map " + resultMapName, e);
				}
			}
		} else if (resultType != null) {
			ResultMap inlineResultMap = new ResultMap.Builder(
					configuration,
					statementId + "-Inline",
					resultType,
					new ArrayList<ResultMapping>(),
					null).build();
			resultMaps.add(inlineResultMap);
		}
		return resultMaps;
	}

	public ResultMapping buildResultMapping(
			Class<?> resultType,
			String property,
			String column,
			Class<?> javaType,
			JdbcType jdbcType,
			String nestedSelect,
			String nestedResultMap,
			String notNullColumn,
			String columnPrefix,
			Class<? extends TypeHandler<?>> typeHandler,
			List<ResultFlag> flags,
			String resultSet,
			String foreignColumn,
			boolean lazy) {
		Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
		TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
		List<ResultMapping> composites = parseCompositeColumnName(column);
		return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
				.jdbcType(jdbcType)
				.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
				.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
				.resultSet(resultSet)
				.typeHandler(typeHandlerInstance)
				.flags(flags == null ? new ArrayList<ResultFlag>() : flags)
				.composites(composites)
				.notNullColumns(parseMultipleColumnNames(notNullColumn))
				.columnPrefix(columnPrefix)
				.foreignColumn(foreignColumn)
				.lazy(lazy)
				.build();
	}

	private Set<String> parseMultipleColumnNames(String columnName) {
		Set<String> columns = new HashSet<String>();
		if (columnName != null) {
			if (columnName.indexOf(',') > -1) {
				StringTokenizer parser = new StringTokenizer(columnName, "{}, ", false);
				while (parser.hasMoreTokens()) {
					String column = parser.nextToken();
					columns.add(column);
				}
			} else {
				columns.add(columnName);
			}
		}
		return columns;
	}

	private List<ResultMapping> parseCompositeColumnName(String columnName) {
		List<ResultMapping> composites = new ArrayList<ResultMapping>();
		if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) {
			StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false);
			while (parser.hasMoreTokens()) {
				String property = parser.nextToken();
				String column = parser.nextToken();
				ResultMapping complexResultMapping = new ResultMapping.Builder(
						configuration, property, column, configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
				composites.add(complexResultMapping);
			}
		}
		return composites;
	}

	private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
		if (javaType == null && property != null) {
			try {
				MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
				javaType = metaResultType.getSetterType(property);
			} catch (Exception e) {
				//ignore, following null check statement will deal with the situation
			}
		}
		if (javaType == null) {
			javaType = Object.class;
		}
		return javaType;
	}

	private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType, JdbcType jdbcType) {
		if (javaType == null) {
			if (JdbcType.CURSOR.equals(jdbcType)) {
				javaType = java.sql.ResultSet.class;
			} else if (Map.class.isAssignableFrom(resultType)) {
				javaType = Object.class;
			} else {
				MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
				javaType = metaResultType.getGetterType(property);
			}
		}
		if (javaType == null) {
			javaType = Object.class;
		}
		return javaType;
	}

	/** Backward compatibility signature */
	public ResultMapping buildResultMapping(
			Class<?> resultType,
			String property,
			String column,
			Class<?> javaType,
			JdbcType jdbcType,
			String nestedSelect,
			String nestedResultMap,
			String notNullColumn,
			String columnPrefix,
			Class<? extends TypeHandler<?>> typeHandler,
			List<ResultFlag> flags) {
		return buildResultMapping(
				resultType, property, column, javaType, jdbcType, nestedSelect,
				nestedResultMap, notNullColumn, columnPrefix, typeHandler, flags, null, null, configuration.isLazyLoadingEnabled());
	}

	public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
		if (langClass != null) {
			configuration.getLanguageRegistry().register(langClass);
		} else {
			langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
		}
		return configuration.getLanguageRegistry().getDriver(langClass);
	}

	/** Backward compatibility signature */
	public MappedStatement addMappedStatement(
			String id,
			SqlSource sqlSource,
			StatementType statementType,
			SqlCommandType sqlCommandType,
			Integer fetchSize,
			Integer timeout,
			String parameterMap,
			Class<?> parameterType,
			String resultMap,
			Class<?> resultType,
			ResultSetType resultSetType,
			boolean flushCache,
			boolean useCache,
			boolean resultOrdered,
			KeyGenerator keyGenerator,
			String keyProperty,
			String keyColumn,
			String databaseId,
			LanguageDriver lang) {
		return addMappedStatement(
				id, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
				parameterMap, parameterType, resultMap, resultType, resultSetType,
				flushCache, useCache, resultOrdered, keyGenerator, keyProperty,
				keyColumn, databaseId, lang, null);
	}

}
